<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');

/**
 * TYPOlight webCMS
 *
 * The TYPOlight webCMS is an accessible web content management system that 
 * specializes in accessibility and generates W3C-compliant HTML code. It 
 * provides a wide range of functionality to develop professional websites 
 * including a built-in search engine, form generator, file and user manager, 
 * CSS engine, multi-language support and many more. For more information and 
 * additional TYPOlight applications like the TYPOlight MVC Framework please 
 * visit the project website http://www.typolight.org.
 * 
 * The Catalog extension allows the creation of multiple catalogs of custom items,
 * each with its own unique set of selectable field types, with field extendability.
 * The Front-End modules allow you to build powerful listing and filtering of the 
 * data in each catalog.
 * 
 * PHP version 5
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 * @license		LGPL 
 * @filesource
 */


/**
 * Class CmAncDescVisitor
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 * Calculates ancestor / descendants for current node
 */
class CmAncDescVisitor
{
    protected $_ancestors = array();
    protected $_descendants = array();
    
    public function preVisit($node)
    {
        if ($node == $node->tree)
        {
            // do not visit container
            return;
        }
        
        if ($node->parent == $node->tree)
        {
            $this->_ancestors[$node->id] = array();
        }
        else
        {
            $this->_ancestors[$node->id] = array_merge($this->_ancestors[$node->parent->id], array($node->parent));
        }
    }
    
    public function postVisit($node)
    {
        if ($node == $node->tree)
        {
            // do not visit container
            return;
        }
        
        $desc = array();
        foreach ($node->children as $child)
        {
            $desc[] = $child;
            $desc = array_merge($desc, $this->_descendants[$child->id]);
        }
        $this->_descendants[$node->id] = array_unique($desc);
    }
    
    public function &__get($key)
    {
        switch ($key)
        {
            case 'ancestors':
                return $this->_ancestors;
                
            case 'descendants':
                return $this->_descendants;
                
            default:
                throw new InvalidArgumentException();
        }
    }
}

/**
 * Class CmNodeFinder
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 * Looks for nodes with specified ids and returns new tree with
 * found nodes as root children.
 */
class CmNodeFinder
{
    private $ids;
    private $tree;
    
    public function __construct(array $ids)
    {
        $this->ids = array_unique($ids);
        $this->tree = new CustomMenuTree();
    }
    
    public function preVisit($node)
    {
        if ($node != $node->tree && in_array($node->id, $this->ids))
        {
            $this->tree->addChild(clone $node);
        }
    }
    
    public function postVisit($node)
    {
    }
    
    public function __get($key)
    {
        switch ($key)
        {
            case 'tree':
                return $this->tree;
                
            default:
                throw new InvalidArgumentException();
        }
    }
}

/**
 * Class CmTreePropertySetter
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 */
class CmTreePropertySetter
{
    private $property;
    private $value;
    
    public function __construct($property, $value)
    {
        $this->property = $property;
        $this->value = $value;
    }
    
    public function preVisit($node)
    {
        $node->$property = $value;
    }
    
    public function postVisit($node)
    {
    }
}

/**
 * Class CmStructureGenerator
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 * Generates page structure according to node settings.
 */
class CmStructureGenerator extends System
{
    
    public function __construct()
    {
        $this->import('Database');
    }
    
    public function preVisit($node)
    {
        if ($node != $node->tree)
        {
            if ($node->type == "tablegen")
            {
                $node->parent->replaceNode($node, 
                    $this->generateTableRows($node));
            }
            
            if ($node->type == "pagegen")
            {
                $node->parent->replaceNode($node, 
                    $this->generatePageStructure($node));
            }
        }
    }
    
    public function postVisit($node)
    {
    }
    
    private function generateTableRows(CmTreeNode $node)
    {
        $query = "SELECT * FROM {$node->tableName} ";
        
        $where = array();
        if ($node->tableWhere)
        {
            $where[] = "({$node->tableWhere})";
        }
        if ($node->defineTableRoots)
        {
            $ids = deserialize($node->tableRoots);
            if (count($ids))
            {
                $col = $node->useTableRootChildren && 
                       $this->Database->fieldExists('pid', $node->tableName) 
                       ? 'pid' : 'id';
                $ids = implode(',', $ids);
                $where[] = "($col in ($ids))";
            }
        }
        if (count($where))
        {
            $query .= "WHERE " . implode(' AND ', $where) . " ";
        }
        
        if ($node->tableOrderBy)
        {
            $query .= "ORDER BY {$node->tableOrderBy}";
        }
        
        $nodes = array();
        $items = $this->Database->execute($query);
        while ($items->next())
        {
            $newNode = new CmTableRowNode($items->row(), 
                $node, $node->tree);
                
            $nodes[] = $newNode;
        }
        
        return $nodes;
    }
    
    private function generatePageStructure(CmTreeNode $node)
    {
        $query = "SELECT * FROM tl_page WHERE type!=? AND type!=? AND hide!=1" .
            ((FE_USER_LOGGED_IN && !BE_USER_LOGGED_IN && !$node->showProtected) ? " AND guests!=1" : "") .
            (!BE_USER_LOGGED_IN ? " AND (start='' OR start<?) AND (stop='' OR stop>?) AND published=1" : "") .
            " ORDER BY sorting";
        $time = time();
        $pages = $this->Database->prepare($query)->execute('error_403', 'error_404', $time, $time);
        
        global $objPage;
                
        $tree = new CustomMenuTree();
        $tree->loadTree($pages->fetchAllAssoc(), 'CmPageNode');
        if ($node->definePageRoots)
        {
            $tree = $tree->fromRoots(deserialize($node->pageRoots));
            if ($node->usePageRootChildren)
            {
                $tree = $tree->prune(1);
            }
        }
        else
        {
            $tree = $tree->fromRoots(array($objPage->trail[0]))->prune(1);
        }
        
        $traces = array();
        if (!$node->hardLimit)
        {
            $traceFinder = new CmTraceFinder();
            $tree->accept($traceFinder);
            
            $traces = $traceFinder->traces;
        }
        
        if ($node->levelOffset > 0 || $node->showLevel > 0)
        {
            $tree = $tree->prune($node->levelOffset, $node->showLevel-1, $traces);
        }
        
        return $tree->children;
    }
}

/**
 * Class CmTreePrinter
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 */
class CmTreePrinter
{
    var $level = 0;
    
    public function preVisit($node)
    {
        if ($node != $node->tree)
        {
            echo str_repeat('--', $this->level), $node->__toString(), '<br/>';
            $this->level++;
        }
    }
    
    public function postVisit($node)
    {
        if ($node != $node->tree)
        {
            $this->level--;
        }
    }
}

/**
 * Class CmPruner
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 * Prunes away all branches in level greater than startLevel
 * and all roots with level lower then endLevel, while keeping
 * all branches in traces intact.
 */
class CmPruner
{
    private $tree;
    private $startLevel;
    private $endLevel;
    private $traces = array();
    
    private $level = 0;
    
    public function __construct($startLevel, $endLevel=-1, array $preservedTraces = array())
    {
        assert($startLevel >= 0);
        
        $this->startLevel = $startLevel;
        $this->endLevel = $endLevel;
        
        foreach ($preservedTraces as $trace)
        {
            foreach ($trace as $node)
            {
                $this->traces[] = $node;
            }
        }
        
        $this->tree = new CustomMenuTree();
    }
    
    public function preVisit($node)
    {
        if ($node != $node->tree)
        {
            if ($this->startLevel == $this->level)
            {
                $node->parent->removeChild($node);
                $this->tree->addChild($node);
            }
            
            if ($this->endLevel != -1 && 
                $this->level >= $this->endLevel)
            {
                foreach ($node->children as $child)
                {
                    if (!in_array($child, $this->traces))
                    {
                        $node->removeChild($child);
                    }
                }
            }
            $this->level++;
        }
    }
    
    public function postVisit($node)
    {
        if ($node != $node->tree)
        {
            $this->level--;
        }
    }
    
    public function &__get($key)
    {
        switch ($key)
        {
            case 'tree':
                return $this->tree;
                
            default:
                throw new InvalidArgumentException();
        }
    }
}

/**
 * Class CmActivePageVisitor
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 */
class CmTraceFinder
{
    private $stack = array();
    private $traces = array();
    
    public function preVisit($node)
    {
        if ($node != $node->tree)
        {
            $this->stack[] = $node;
         
            if ($node->isActive())
            {
                $this->traces[] = $this->stack;
            }
        }
    }
    
    public function postVisit($node)
    {
        if ($node != $node->tree)
        {
            array_pop($this->stack);
        }
    }
    
    public function &__get($key)
    {
        switch ($key)
        {
            case 'traces':
                return $this->traces;
                
            default:
                throw new InvalidArgumentException();
        }
    }
}

/**
 * Class CmTreeNode
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 */
class CmTreeNode 
{
	protected $_data = array();
    
    protected $_parent = null;
    
    protected $_children = array();
    
    protected $_tree = null;
    
    public function __construct($data, $tree, $parent = null)
    {
        $this->_parent = $parent;
        $this->_tree = $tree;
        $this->_data = $data;        
    }
    

    public function __clone()
    {
        $children = $this->_children;
        $this->_tree = null;
        $this->_children = array();
        foreach ($children as $child)
        {
            $this->addChild(clone $child);
        }
    }
    
    public function addChild(CmTreeNode $node)
    {
        assert(is_null($node->_tree) || $this->_tree == $node->_tree);
        
        $node->_parent = $this;
        $this->_children[] = $node;
        
        if (is_null($node->_tree) && !is_null($this->_tree))
        {
            $this->setTree($this->_tree);
        }
    }
    
    public function addChildren(array $nodes)
    {
        foreach ($nodes as $node)
        {
            $this->addChild($node);
        }
    }
    
    public function removeChild(CmTreeNode $node)
    {
        $index = array_search($node, $this->_children);
        assert($index !== false);
        
        array_splice($this->_children, $index, 1);
        
        $node->_parent = null;
        $node->_tree = null;
    }
    
    public function clearChildren()
    {
        foreach ($this->_children as $node)
        {
            $node->setTree(null);
        }
        $this->_children = array();
    }
    
    public function replaceNode(CmTreeNode $node, array $nodes)
    {
        assert($node != $this);
        
        $index = array_search($node, $this->_children);
        assert($index !== false);
        
        foreach ($nodes as $node)
        {
            $node->setTree($this->_tree);
            $node->_parent = $this;
        }

        array_splice($this->_children, $index, 1, $nodes);
    }
    
    public function __get($key)
    {
        switch ($key)
        {
            case 'parent':
                return $this->_parent;
                
            case 'children':
                return $this->_children;
                
            case 'tree':
                return $this->_tree;
                
            default:
                return isset($this->_data[$key]) ? $this->_data[$key] : null;
        }
    }
    
    public function __set($key, $value)
    {
        $this->_data[$key] = $value;
    }
    
    public function __toString()
    {
        return $this->_data['title'];
    }
    
    public function accept($visitor)
    {        
        $visitor->preVisit($this);
    
        foreach ($this->_children as $child)
        {
            $child->accept($visitor);
        }
        
        $visitor->postVisit($this);
    }
    
    public function isActive()
    {
        return false;
    }
    
    private function setTree($tree)
    {
        $this->_tree = $tree;
        foreach ($this->_children as $child)
        {
            $child->setTree($tree);
        }
    }
}

class CmTableRowNode extends CmTreeNode
{
    private $meta;
    
    public function __construct($data, $meta, $tree, $parent = null)
    {
        $this->meta = $meta;
        parent::__construct($data, $tree, $parent);
    }
    
    public function __toString()
    {
        $name = $this->meta->tableColumn;
        return $this->$name;
    }
    
    public function isActive()
    {
        global $objPage;
        
        //var_dump($this->meta);
        
        return false;
    }
}

class CmPageNode extends CmTreeNode
{
    public function isActive()
    {
        global $objPage;
        return $this->id == $objPage->id || 
            $this->type == "forward" && $this->jumpTo == $objPage->id;
    }
    
    public function __toString()
    {
        return $this->title;
    }
}

/**
 * Class CustomMenuTree
 *
 * @copyright	Martin Komara 2009
 * @author		Martin Komara, 
 * @package		CustomMenu
 *
 */
class CustomMenuTree extends CmTreeNode
{
	public function __construct()
    {
        parent::__construct(array(), $this);
    }
    
    public function loadTree($items, $nodeClass = 'CmTreeNode')
    {
        $this->_children = array();
        $this->addChildren($this->loadNodes($items, $nodeClass));
    }
    
    public function loadNodes($items, $nodeClass = 'CmTreeNode')
    {
        $map = array();
        
        $nodes = array();
        $rootNodes = array();
        
        foreach ($items as $item)
        {
            $nodes[] = $map[$item['id']] = new $nodeClass($item, $this);
        }
        
        foreach ($nodes as $node)
        {
            if ($node->pid && !isset($map[$node->pid]))
            {
                continue;
            }
            
            if ($node->pid)
            {
                $parent = $map[$node->pid];
                $parent->addChild($node);
            }
            else
            {
                $rootNodes[] = $node;
            }
        }
        
        return $rootNodes;
    }
    
    public function fromRoots(array $roots)
    {
        $visitor = new CmNodeFinder($roots);
        $this->accept($visitor);
        
        return $visitor->tree;
    }
    
    public function generate()
    {
        $this->accept(new CmStructureGenerator());
    }
    
    public function prune($startLevel, $endLevel=-1, array $preservedTraces=array())
    {
        $visitor = new CmPruner($startLevel, $endLevel, $preservedTraces);
        $this->accept($visitor);
        
        return $visitor->tree;
    }
}

?>
